Skip to content

feat: 詳細ログの URL をクリックでブラウザで開けるようにする - #90

Merged
takecchi merged 2 commits into
mainfrom
codiva/url-command
Aug 4, 2026
Merged

feat: 詳細ログの URL をクリックでブラウザで開けるようにする#90
takecchi merged 2 commits into
mainfrom
codiva/url-command

Conversation

@takecchi

@takecchi takecchi commented Aug 4, 2026

Copy link
Copy Markdown
Owner

概要

セッション詳細のログに出てくる URL を下線付きで表示し、クリックするとブラウザで開くようにしました。Markdown のリンク([ラベル](URL))はラベルをクリックするとリンク先へ飛びます。

折り返しで URL が 2 行に分かれていても、どちらの行をクリックしても URL 全体が開きます。

なぜ Cmd+クリックではなく「普通のクリック」なのか

当初のご要望は Cmd+クリックでしたが、端末のリンク機能には任せられないことが調査で分かったため、codiva 自身がクリックを取って開く方式にしました。

事実 根拠
Ghostty はマウス捕捉中にリンク検出を止める Surface.zigcursorPosCallbackmouse_event != .none のときホバー判定(mouseRefreshLinks)を skip する。over_link が立たないのでクリックも成立しない。裸 URL の自動検出も同じ経路
SGR マウスレポートに Cmd/Super のビットが無い xterm の修飾ビットは shift=4 / alt=8 / ctrl=16 のみ。Ghostty の mouse_encode.zig は Cmd を一切エンコードしない(= 素のクリックとバイト列が同一)
bit 8 の意味は端末で違う Ghostty/xterm では Option、iTerm2 では Cmd(VT100Output.m)。判定に使えない
素の左クリックはどの端末でも届く → これが唯一どこでも同じに動く経路

codiva は全画面のあいだ ?1002/?1006 を有効にしているので、Ghostty(主端末)では素の Cmd+クリックは設定でも直せません(Shift を足すか捕捉を切るしかない)。

OSC 8 ハイパーリンクも併せて出力しているので、対応端末では端末側のクリックも使えます:

端末 端末側のリンククリック
iTerm2 / GNOME Terminal / Windows Terminal Cmd+クリック / Ctrl+クリック
Ghostty / kitty / WezTerm **Shift+**Cmd+クリック(Shift が捕捉のバイパスキー)
非対応端末 エスケープを無視するだけ(表示は変わらない)

調査結果は docs/TECH_NOTES.md に実測付きで残しました。

実装

  • core/url.ts(新規・純粋) — URL 検出 / 行内のリンク範囲 / OSC 8 の組み立て
  • リンクは行ごとの範囲として持つDisplayLine.links)。表示テキストから URL を引き直さないのは (1) [label](url) は見えているのが label なので復元できない、(2) 折り返しで割れた半分は URL として解析できない、の 2 点。範囲なら両方の行が URL 全体を指せる
  • 検出は必ず論理行に対して行い、linksInSlice で各物理行の座標(prefix / 字下げのぶんずらす)へ移す
  • OSC 8 は描画時にだけ混ぜるLogEntry.text に入れると wrapDisplayLines がエスケープを可視幅として数えて折り返しが壊れる(幅 20 で URI が 4 行に割れるのを実測)
  • 選択境界とリンク境界は直交するので純粋な関数で 2 段に切る(linkPiecesselectionSlices

誤爆しないための作り

  • press では開かず release で開くdrag が来たら取り消す)→ 範囲選択の操作でブラウザが立ち上がらない
  • 当たり判定は行末で丸めない → URL で終わる行の右の余白をクリックしても開かない
  • 左ボタンだけMouseEvent.button を追加)→ 右クリック・中クリックでは開かない
  • モーダル表示中はマウスレポートを飲む(一覧と同じ方針)→ 許可待ちダイアログの上のクリックで開かない
  • http(s) のみ(file: / javascript: / mailto: は開かない)、制御文字を含む URL は弾く

併せて直したバグ(別コミット)

レビュー中に見つけた既存のバグfix: コミットで先に直しています。caretIndexForColumn がコードポイント単位で歩いていたため、異体字セレクタ付き絵文字(⚠️ = U+26A0 + U+FE0F。アシスタントの Markdown に頻出)を含む行で当たり判定が 1 セルずれていました。

'⚠️ check https://x.dev/a for details'
             ^ 修正前: URL 手前の空白をクリックすると開く(誤爆)
                             ^ 修正前: URL の最後の文字が反応しない

折り返しはグラフェム単位で測っているので、逆写像だけ単位が違っていた形です。ログの範囲選択・ヘッダ・コンポーザのクリックにも影響していました(グラフェムの途中に caret が入り、slice した選択が壊れた絵文字を含む問題も同時に解消)。共有の分割器を core/graphemes.ts に置き、幅を数える箇所すべてが同じ単位を通るようにしました。

テスト

lint / typecheck / test / build の 4 点を通しています(2129 tests passed、カバレッジは閾値以上)。

  • src/core/url.spec.ts(新規・テーブルドリブン)— 検出・末尾の句読点・括弧の釣り合い・スキーム絞り込み・範囲の切り出し・linkPieces の文字保存
  • src/core/scroll.spec.ts — prefix ぶんのずれ / 折り返しで割れた URL / Markdown の href / コードブロック内の裸 URL / 隣り合う別リンク
  • src/core/log-selection.spec.tslogLinkAt の端(URL の先頭・末尾・直後・行末より右)、CJK を含む行
  • src/core/text-buffer.spec.ts — グラフェム単位の逆算(VS16 絵文字の回帰テスト)
  • tests/app.test.tsx — 実際に SGR マウスレポートを流して「開く / 開かない」を検証(URL 上のクリック / URL 外 / 行末より右 / ドラッグ / 右・中クリック)

手動確認のお願い

実セッション(TTY + Claude 認証)が必要な体感確認は未実施です。お手数ですが、実際の Ghostty で以下をご確認いただけると助かります。

  • ログ内の URL が下線付きで表示される
  • URL をクリックしてブラウザが開く
  • URL の上からドラッグして範囲選択したときは開かない(コピーだけされる)
  • URL 以外の場所・行末より右のクリックで開かない
  • ⚠️ などの絵文字を含む行でも、クリックした文字にきちんと当たる

`caretIndexForColumn` がコードポイント単位で歩いていたため、異体字セレクタ付きの
絵文字(`⚠️` = U+26A0 + U+FE0F。アシスタントの Markdown に頻出)を含む行で、以降の
当たり判定が 1 セルずれていた。折り返し(`wrapDisplayLines` / `wrapRichLine`)は
グラフェム単位で測っているので、逆写像だけ単位が違っていたことになる。

- `⚠️` は 1 グラフェム = 2 セルだが、コードポイントごとに測ると 1 + 0 = 1 セル
- 結果、ログの範囲選択・ヘッダ・コンポーザのクリックが隣の文字に当たっていた
- グラフェムの途中に caret が入るため、`slice` した選択が壊れた絵文字を含んでいた

共有の分割器を `core/graphemes.ts` に置き、幅を数える箇所すべてが同じ単位を通るように
する。併せて「行末より右は当たりにしない」`charIndexAtColumn` を追加した(判定と逆算を
1 回の走査でまとめて行う — 別々に測ると同じ単位の食い違いが再発する)。
セッション詳細のログに出てくる URL を下線付きで表示し、クリックすると
ブラウザで開くようにした。Markdown のリンク(`[ラベル](URL)`)はラベルを
クリックするとリンク先へ飛ぶ。

## なぜ Cmd+クリックではないか

端末のリンク機能に任せられないため、codiva 自身がクリックを取って開く。

- **Ghostty はマウス捕捉中にリンク検出を止める**(`Surface.zig` が
  `mouse_event != .none` でホバー判定を skip する)。codiva は全画面のあいだ
  `?1002`/`?1006` を有効にしているので、素の Cmd+クリックは効かない
- **SGR マウスレポートに Cmd/Super のビットが無い**(shift=4 / alt=8 / ctrl=16 のみ)。
  しかも bit 8 の意味は端末で違う(Ghostty=Option / iTerm2=Cmd)ので判定に使えない
- 一方**素の左クリックはどの端末でもアプリに届く**ので、これが唯一どこでも同じに
  動く経路になる

OSC 8 ハイパーリンクも併せて出力するので、対応端末(iTerm2 / VTE /
Windows Terminal)では端末側の Cmd/Ctrl+クリックも使える。Ghostty / kitty /
WezTerm は Shift を足した Shift+Cmd+クリックで使える。調査の詳細は
`docs/TECH_NOTES.md`。

## 実装

- `core/url.ts`(純粋): URL 検出・行内のリンク範囲・OSC 8 の組み立て
- リンクは**行ごとの範囲**として持つ(`DisplayLine.links`)。表示テキストから
  URL を引き直さないのは (1) `[label](url)` は見えているのが label なので
  復元できない、(2) 折り返しで割れた半分は URL として解析できない、の 2 点。
  範囲なら両方の行が URL 全体を指せるのでどちらをクリックしても同じ先へ飛ぶ
- 検出は必ず論理行に対して行い、`linksInSlice` で各物理行の座標へ移す
- **OSC 8 は描画時にだけ混ぜる**。`LogEntry.text` に入れると折り返しが
  エスケープを可視幅として数えて壊れる(幅 20 で URI が 4 行に割れた)
- press では開かず release で開く(drag が来たら取り消す)ので、範囲選択の
  操作でブラウザが立ち上がらない
- 当たり判定は行末で丸めない(URL で終わる行の右の余白で開かないため)
- 副作用は左ボタンだけ(`MouseEvent.button` を追加)。右クリック・中クリックでは開かない
- モーダル表示中は詳細ビューもマウスレポートを飲む(一覧と同じ方針)
@takecchi
takecchi merged commit 22ee4e4 into main Aug 4, 2026
1 check passed
@takecchi
takecchi deleted the codiva/url-command branch August 4, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant